﻿
using Boilen.Primitives.Members;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Windows;
using System.Xml.Linq;


namespace Boilen.Primitives.Implementers {

    /// <summary>
    /// Implements the <see cref="IEquatable{T}"/> interface.
    /// </summary>
    public sealed class EquatableInterface : InterfaceImplementer<IEquatable<object>> {

        private static readonly Type InterfaceType = typeof( IEquatable<object> );

        private readonly bool isBaseEquatable_;


        /// <summary>
        /// Gets or sets a value indicating whether to implement the <c>==</c> and <c>!=</c> equality operators.
        /// </summary>
        public bool ImplementOperators { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether the type provides a custom <c>Equals</c> method implementation.
        /// </summary>
        public Implementation EqualsImplementation { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether the type provides a custom <c>GetHashCode</c> method implementation.
        /// </summary>
        public Implementation GetHashCodeImplementation { get; set; }

        /// <summary>
        /// Gets or sets a value indicating whether to include an exact type check on the other object.
        /// </summary>
        public bool VerifyExactType { get; set; }

        /// <summary>
        /// Gets or sets a function used to select the format string for comparing or getting the hash code of each equatable property.
        /// </summary>
        /// <remarks>
        /// The second parameter will be <see langword="true"/> for <see cref="EqualsImplementation"/>, and <see langword="false"/> for <see cref="GetHashCodeImplementation"/>.
        /// </remarks>
        public Func<ITarget, bool, string> FormatSelector { get; set; }


        /// <inheritdoc/>
        protected override IEnumerable<Member> InterfaceMembers {
            get {
                var equatableProperties = this.GetTargetData( ( ITarget t ) => t.Equatable, ( ITarget t ) => t );
                Ensure.Satisfies( this.EqualsImplementation.HasFlag( Implementation.Custom ) || equatableProperties.Any( ), "No equatable properties found." );

                // Implement "IEquatable<T>.Equals( T )" interface method.
                yield return this.CreateInterfaceEqualsMethod( equatableProperties );

                // Implement operators, if requested.
                if( this.ImplementOperators ) {
                    yield return this.CreateOperatorMethod( true );
                    yield return this.CreateOperatorMethod( false );
                }

                // Override object equality methods.
                if( this.EqualsImplementation != Implementation.None ) {
                    if( this.isBaseEquatable_ )
                        yield return this.CreateInterfaceEqualsOverride( );
                    else
                        yield return this.CreateObjectEqualsOverride( );
                }

                if( this.GetHashCodeImplementation != Implementation.None ) {
                    yield return this.CreateObjectGetHashCodeOverride( equatableProperties );
                }
            }
        }


        /// <inheritdoc/>
        public EquatableInterface( PartialType parent )
            : base( parent, parent.TypeRepository.GetTypeName( InterfaceType ).Replace( "<object>", "<" + parent.TypeName + ">" ) ) {
            // Ensure all interface types are registered.
            this.Parent.TypeRepository.AddNamespace( typeof( EqualityComparer<> ) );

            Type baseType = this.Parent.BaseType;
            this.isBaseEquatable_ = baseType.GetInterfaces( )
                .Where( i => i.IsGenericType )
                .Select( i => i.GetGenericTypeDefinition( ) )
                .Contains( typeof( IEquatable<> ) );

            this.ImplementOperators = !this.isBaseEquatable_;
            this.EqualsImplementation = Implementation.Auto;
            this.GetHashCodeImplementation = Implementation.Auto;
        }


        /// <inheritdoc/>
        public override void Prepare( ) {
            bool isDependencyObject = typeof( DependencyObject ).IsAssignableFrom( this.Parent.Type );
            Ensure.Satisfies( !isDependencyObject, "Cannot implement IEquatable<T> on type derived from DependencyObject." );

            base.Prepare( );
            this.PrepareTargets( ( ITarget t ) => t.Prepare( this ) );
        }

        /// <summary>
        /// Assigns the values used for the <see cref="ImplementOperators"/> property.
        /// </summary>
        [DefaultValue( "<none>" )]
        public EquatableInterface SetImplementOperators( bool implementOperators ) {
            this.ImplementOperators = implementOperators;
            return this;
        }

        /// <summary>
        /// Assigns the values used for the <see cref="EqualsImplementation"/> property.
        /// </summary>
        [DefaultValue( "Auto,Custom,AutoAndCustom" )]
        public EquatableInterface SetEqualsImplementation( Implementation equalsImplementation ) {
            this.EqualsImplementation = equalsImplementation;
            return this;
        }

        /// <summary>
        /// Assigns the values used for the <see cref="GetHashCodeImplementation"/> property.
        /// </summary>
        [DefaultValue( "Auto,Custom,AutoAndCustom" )]
        public EquatableInterface SetGetHashCodeImplementation( Implementation getHashCodeImplementation ) {
            this.GetHashCodeImplementation = getHashCodeImplementation;
            return this;
        }

        /// <summary>
        /// Assigns the values used for the <see cref="ImplementOperators"/> property.
        /// </summary>
        public EquatableInterface SetVerifyExactType( bool verifyExactType ) {
            this.VerifyExactType = verifyExactType;
            return this;
        }

        /// <summary>
        /// Assigns the values used for the <see cref="FormatSelector"/> property.
        /// </summary>
        public EquatableInterface SetFormatSelector( Func<ITarget, bool, string> formatSelector ) {
            this.FormatSelector = formatSelector;
            return this;
        }


        private MethodMember CreateMethod( string name, string returnType, BlockMember body, XElement existingDoc, string additionalModifiers, params ParameterMember[] parameters ) {
            var method = new MethodMember( name, returnType, body ) {
                Doc = this.CreateDoc( )
            }.AddParameters( parameters );

            if( existingDoc == null )
                method.Doc.InheritFrom = "";
            else
                method.Doc.AddDocElements( XmlDocumentation.FilterElements( existingDoc, "param", "exception" ) );

            if( !string.IsNullOrEmpty( additionalModifiers ) )
                method.Modifiers += " " + additionalModifiers;

            return method;
        }


        private MethodMember CreateInterfaceEqualsMethod( IEnumerable<ITarget> equatableProperties ) {
            var typeRepository = this.Parent.TypeRepository;
            var interfaceEquals = InterfaceType.GetGenericTypeDefinition( ).GetMethod( "Equals" );
            var parameter = MethodMember.GetParameters( interfaceEquals, this.CreateParameter ).Single( );

            string parameterName = parameter.Name;
            string methodName = interfaceEquals.Name;
            string returnType = typeRepository.GetTypeName( interfaceEquals.ReturnType );
            var formatSelector = this.FormatSelector ?? (( p, e ) => null);

            var interfaceEqualsBody = new BlockMember( methodName, w => {
                w.Write( "return " );
                using( Enclose.Indent( w ) ) {
                    if( this.isBaseEquatable_ ) {
                        w.WriteLine( "base.Equals({0})", parameterName );
                        w.Write( "&& " );
                    }
                    else if( !this.Parent.Type.IsValueType ) {
                        w.WriteLine( "!object.ReferenceEquals({0}, null)", parameterName );
                        w.Write( "&& " );
                    }

                    if( this.VerifyExactType && !this.Parent.Type.IsValueType ) {
                        w.WriteLine( "this.GetType() == {0}.GetType()", parameterName );
                        w.Write( "&& " );
                    }

                    var targets = GetTargets( equatableProperties, this.EqualsImplementation );
                    Util.Iterate(
                        targets,
                        ( i, last ) => { w.WriteLine( ); w.Write( "&& " ); },
                        ( i, p ) => {
                            if( p == null ) {
                                w.Write( "this.EqualsCore({0})", parameterName );
                            }
                            else {
                                string format = formatSelector( p, true ) ?? "EqualityComparer<{2}>.Default.Equals(this.{1}, {0}.{1})";
                                w.Write( format, parameterName, p.PropertyName, p.TypeName );
                            }
                        }
                    );

                    w.WriteLine( ";" );
                }
            } );

            string modifiers = this.Parent.IsSealed || this.EqualsImplementation.HasFlag( Implementation.Custom ) ? null : "virtual";
            var interfaceEqualsDoc = XmlDocumentation.GetDocMember( interfaceEquals );
            return this.CreateMethod( methodName, returnType, interfaceEqualsBody, interfaceEqualsDoc, modifiers, parameter );
        }

        private MethodMember CreateInterfaceEqualsOverride( ) {
            var typeRepository = this.Parent.TypeRepository;
            string baseTypeName = this.Parent.BaseTypeName;
            var interfaceEquals = InterfaceType.GetGenericTypeDefinition( ).GetMethod( "Equals" );
            var parameter = MethodMember.GetParameters( interfaceEquals, ( name, type, description ) => new ParameterMember( name, baseTypeName ) ).Single( );

            string parameterName = parameter.Name;
            string methodName = interfaceEquals.Name;
            string returnType = typeRepository.GetTypeName( interfaceEquals.ReturnType );

            var interfaceEqualsBody = new BlockMember( methodName, w => w.WriteLine( "return this.Equals({0} as {1});", parameterName, this.Parent.TypeName ) );
            var method = this.CreateMethod( methodName, returnType, interfaceEqualsBody, null, "override", parameter );
            method.Attributes.Add( AttributeMember.EditorBrowsableNever );
            return method;
        }

        private MethodMember CreateOperatorMethod( bool isEqualityOperator ) {
            const string ReferenceTypeDoc = "%see:parent_type%";
            const string OperatorImplementationFormat = "return {0}EqualityComparer<{2}>.Default.Equals({1});";

            string operatorName = "operator " + (isEqualityOperator ? "==" : "!=");
            string operatorDoc = isEqualityOperator ? "the same value" : "different values";
            string parameterSuffix = this.Parent.Type.IsValueType ? "" : " or null";

            var leftParameter = new ParameterMember( "left", this.Parent.TypeName );
            var rightParameter = new ParameterMember( "right", this.Parent.TypeName );

            string argumentString = leftParameter.Name + ", " + rightParameter.Name;
            var operatorBody = new BlockMember( operatorName, w =>
                w.WriteLine( OperatorImplementationFormat, isEqualityOperator ? "" : "!", argumentString, this.Parent.TypeName )
            );

            var operatorMethod = new MethodMember( operatorName, "bool", operatorBody ) {
                Doc = this.CreateDoc( )
                    .AddSummary( "Determines whether two specified {0} objects have {1}.", ReferenceTypeDoc, operatorDoc )
                    .AddReturns( "true if the value of %paramref:left% is the same as the value of %paramref:right%; otherwise, false." )
                    .AddParam( leftParameter.Name, "A {0} object{1}.", ReferenceTypeDoc, parameterSuffix )
                    .AddParam( rightParameter.Name, "A {0} object{1}.", ReferenceTypeDoc, parameterSuffix ),
                Modifiers = "public static",
                Parameters = { leftParameter, rightParameter }
            };

            return operatorMethod;
        }

        private MethodMember CreateObjectEqualsOverride( ) {
            var objectEqualsMethod = typeof( object ).GetMethod( "Equals", new[] { typeof( object ) } );
            var parameter = MethodMember.GetParameters( objectEqualsMethod, this.CreateParameter ).Single( );

            string parameterName = parameter.Name;
            string typeName = this.Parent.TypeName;
            string methodName = objectEqualsMethod.Name;
            string returnType = this.Parent.TypeRepository.GetTypeName( objectEqualsMethod.ReturnType );

            var objectEqualsBody = new BlockMember( methodName, w => {
                if( this.Parent.Type.IsValueType ) {
                    w.WriteLine( "return {0} is {1}", parameterName, typeName );
                    using( Enclose.Indent( w ) )
                        w.WriteLine( "&& this.Equals(({1}){0});", parameterName, typeName );
                }
                else {
                    w.WriteLine( "return this.Equals({0} as {1});", parameterName, typeName );
                }
            } );

            string modifiers = (this.Parent.Type.IsValueType ? "" : "sealed ") + "override";
            var objectEqualsDoc = XmlDocumentation.GetDocMember( objectEqualsMethod );
            return this.CreateMethod( methodName, returnType, objectEqualsBody, objectEqualsDoc, modifiers, parameter );
        }

        private MethodMember CreateObjectGetHashCodeOverride( IEnumerable<ITarget> equatableProperties ) {
            // http://musingmarc.blogspot.com/2008/03/sometimes-you-make-hash-of-things.html
            var objectGetHashCodeMethod = typeof( object ).GetMethod( "GetHashCode", Type.EmptyTypes );

            string methodName = objectGetHashCodeMethod.Name;
            string returnType = this.Parent.TypeRepository.GetTypeName( objectGetHashCodeMethod.ReturnType );
            var formatSelector = this.FormatSelector ?? (( p, e ) => null);

            var objectGetHashCodeBody = new BlockMember( methodName, w => {
                w.Write( returnType + " hash = " );
                if( this.isBaseEquatable_ ) {
                    w.WriteLine( "base.GetHashCode();" );
                    w.Write( "hash = " );
                }

                var targets = GetTargets( equatableProperties, this.GetHashCodeImplementation );
                Util.Iterate(
                    targets,
                    ( i, last ) => w.Write( "hash = " ),
                    ( i, p ) => {
                        if( this.isBaseEquatable_ || i > 0 )
                            w.Write( "((hash << 5) + hash) ^ " );

                        if( p == null ) {
                            w.Write( "this.GetHashCodeCore()" );
                        }
                        else {
                            string format = formatSelector( p, false ) ?? "EqualityComparer<{1}>.Default.GetHashCode(this.{0})";
                            w.Write( format, p.PropertyName, p.TypeName );
                        }

                        w.WriteLine( ";" );
                    }
                );
                w.WriteLine( "return hash;" );
            } );

            string modifiers = (this.Parent.Type.IsValueType || !this.GetHashCodeImplementation.HasFlag( Implementation.Custom ) ? "" : "sealed ") + "override";
            var objectGetHashCodeDoc = XmlDocumentation.GetDocMember( objectGetHashCodeMethod );
            return this.CreateMethod( methodName, returnType, objectGetHashCodeBody, objectGetHashCodeDoc, modifiers );
        }

        private static IEnumerable<ITarget> GetTargets( IEnumerable<ITarget> equatableProperties, Implementation implementation ) {
            var targets = new List<ITarget>( );

            if( implementation.HasFlag( Implementation.Auto ) )
                targets.AddRange( equatableProperties );

            if( implementation.HasFlag( Implementation.Custom ) )
                targets.Add( null );

            return targets;
        }


        /// <summary>
        /// Represents an <see cref="IImplementer"/> that supports the <see cref="IEquatable{T}"/> interface.
        /// </summary>
        public interface ITarget : IImplementer {
            /// <summary>
            /// Gets a value indicating whether a property should be used when determining the equality of the parent type.
            /// </summary>
            bool Equatable { get; set; }

            /// <summary>
            /// Gets the name of the equatable property represented by the target.
            /// </summary>
            string PropertyName { get; }

            /// <summary>
            /// Called by <see cref="EquatableInterface"/> to prepare a target implementer for the interface.
            /// </summary>
            void Prepare( EquatableInterface implementer );
        }

    }

}
